TypeScript
Wat is TypeScript
TypeScript is een superset van JavaScript. Het bouwt eigenlijk een laag bovenop JavaScript die ervoor zorgt dat we static typing kunnen toevoegen.
Met static typing bedoelen we dat de types van variabelen al gecontroleerd worden tijdens het schrijven van de code. Je moet heel expliciet maken, terwijl je code schrijft, wat de verwachtingen zijn qua type voor elke variabele of functie die je definieert.
JavaScript daarentegen is van nature dynamisch getypt. De type van een variabele wordt pas gecheckt als het programma runt en kan daarom tijdens het runnen van het programma veranderen.
// JAVASCRIPT
let greeting = "Hello"; // greeting is een string
greeting = 10 // er wordt een number in greeting opgeslagen -> Geen error
// TYPESCRIPT
let greeting = "Hello"; // greeting is een string
greeting = 10 // er wordt GEPROBEERD een number in greeting op te slaan -> DIT GOOIT EEN ERROR
Statisch typen biedt enkele belangrijke voordelen:
- Je kan vroeger fouten detecteren: Als een functie of een variabele verkeerd gebruikt wordt dan kan je dit zien tijdens het schrijven van de code
- Betere toolen: Je IDE gaat slimmere autocomplete kunnen toepassen op je code
- Leesbaarheid: De code wordt ineens veel duidelijker omdat een variabele nu met een gepaste naam en type geïnitialiseerd worden waardoor het voor alle developer veel duidelijker is hoe het gebruikt moet worden
- Minder error prone: Tijdens het schrijven kunnen al veel errors uit de code gehaald worden die niet meer tijdens het runnen van het programma problemen geven
Belangrijk om te weten is dat de JavaScript runtimes zoals NodeJS of je browser alleen maar JavaScript begrijpen en kunnen runnen. Dat betekent dat als je TypeScript files schrijft, je code eerst terug naar JavaScript moet getranspiled worden voor het kan runnen. Hier zijn gelukkig tools voor.
TypeScript installeren
Er zijn 2 opties om typescript te gaan gebruiken in een project:
- Globaal installeren
- Lokaal in je project installeren
We overlopen kort even de voor- en nadelen van beide en de stappen die nodig zijn.
Mogelijkheid 1 - TypeScript globaal installeren
Voordelen:
- Je kan van eender welke terminal en eender welke plaats in je file systeem van je computer JS files naar TS files transpilen
Nadelen:
- Je installeert 1 versie van TS die je overal moet gebruiken. Je kan dus niet verschillende versies in verschillende projecten gebruiken
- Als iemand anders je project binnenhaalt en die heeft geen typescript globaal geïnstalleerd zal dit problemen geven
Werkwijze: Open een terminal op je computer en run het volgende:
npm install -g typescript
Opmerking: NPM is een package manager zoals in een van de andere lessen besproken. Wat we hier zeggen tegen NPM is om een library (package) typescript te installeren, en met de -g flag zeggen we dat we dit globaal in ons systeem beschikbaar willen hebben.
Mogelijkheid 2 - TypeScript in een folder gebruiken
Voordelen:
- Je kan per project verschillende versies van TypeScript gebruiken
- Wanneer iemand je repository cloned en
npm installdoet (of iets vergelijkbaar) dan is TypeScript ineens geïnstalleerd
Nadelen:
- Je kan niet van eender waar op je computer TypeScript files naar JavaScript files transpileren
Werkwijze:
- Stap 1 - NPM kunnen gebruiken en TypeScript in je project installeren
npm init #optional - als je met een gestructureerde project structuur wilt werken
npm install typescript
- Stap 2 - Initialiseren TypeScript
npx tsc --init #npx is een tool die helpt om lokaal geinstalleerde commandos te runnen alsof ze globaal zijn
Dit maakt een tsconfig.json file aan, waar je veel opties ziet. Voorlopig mag je veronderstellen dat de defaults wel goed zijn. Er is uitgebreide documentatie over wat al deze settings betekenen.
- Stap 3 - Runnen van TypeScript files
npx tsc index.ts && node index.js
Om een typescript file te runnen zijn er twee stappen nodig. Eerst gaan we met tsc de typescript file transpilen en vervolgens runnen we de javascript file die daaruit komt met node.
Basic Types
Primitieve types
In TypeScript hebben we een paar built-in basis types die we de primitieve types (=primitive types) noemen:
numberstringbooleannullundefinedanyunknown
Een type definiëren doe je aan de hand van de : notatie na een variabele.
let isDone: boolean = false;
let age: number = 30;
let name: string = "John";
Arrays van primitive types
Je kan op een gelijkaardige manier Arrays van primitive types definiëren:
let list: number[] = [1, 2, 3];
let stringList: string[] = ["one", "two", "three"];
Enums
Een al iets specialer type is een Enum. Een Enum wordt gebruikt om een eindige set van mogelijkheden te omvatten. Bijvoorbeeld:
enum Color {
Red,
Green,
Blue,
}
let c: Color = Color.Green;
Type Inference
Type inference wijst op het feit dat TypeScript in sommige gevallen kan afleiden wat de type van een variabele is zonder dat het expliciet vermeld wordt.
let age: number;
age = 30
// Kan je ook korter schrijven als
let age = 30; // TypeScript weet dat 'age' een 'number' is
Dit is een belangrijk voordeel van typescript want het vermindert de noodzaak om types overal expliciet te maken, zonder dat je je type safety verliest.
Functies in TypeScript
Het definiëren van een functie in TypeScript kan je doen door alle input parameters en return waarden te typen:
function greet(name: string): string {
return `Hello, ${name}!`;
}
In deze functie definieer je dat de input parameter name een type string moet hebben en dat deze altijd een string moet returnen.
Dat betekent dat als je een van de volgende dingen zou doen terwijl je code schrijft, je typescript een error gooit:
- Als je in de functie zelf vergeet om een string te returnen
- Als je de functie zou oproepen met een variable die niet van het type string is, bv
greet(10)
Een functie definitie in typescript kan ook default of optionele input parameters hebben:
function greet(name: string, age: number = 25): string {
return `Hello, ${name}, you are ${age} years old!`;
}
function greet(name: string, age?: number): string {
return `Hello, ${name}, you are ${age} years old!`;
}
Let op: een parameter kan niet zowel optioneel zijn, als een default waarde hebben. Bovendien moeten de optionele parameters altijd achteraan in de functie input staan:
// DIT GEEFT EEN ERROR - let op de plaats van de optionele parameter
function greet(name: string, age?: number, year: number): string {
return `Hello, ${name}, you are ${age} years old!`;
}
// DIT IS CORRECT - let op de plaats van de optionele parameter
function greet(name: string, year: number, age?: number): string {
return `Hello, ${name}, you are ${age} years old!`;
}
Types en Interfaces
In TypeScript kunnen zowel types als interfaces gebruikt worden om de vorm van objecten en andere structuren te definiëren. Ze lijken op elkaar, maar er zijn enkele subtiele verschillen en specifieke scenario's waarin je de ene boven de andere zou kiezen.
Wanneer een interface
Objectstructuren en Klassen
Gebruik een interface wanneer je een objectstructuur of klasse wilt definiëren. Interfaces worden vaak gebruikt voor objecten en bieden een duidelijke manier om de vorm van objecten te beschrijven.
interface Person {
name: string,
age: number,
isAdmin: boolean
}
Uitbreiding (Inheritance/extending)
Interfaces ondersteunen ingebouwde uitbreiding (inheritance) van andere interfaces, wat het makkelijk maakt om objecten uit te breiden zonder duplicatie van code. Interfaces kunnen eenvoudig worden uitgebreid door andere interfaces of klassen.
interface Person {
name: string,
age: number,
isAdmin: boolean
}
interface Employee extends Person {
employeeId: number;
jobTitle: string;
}
Samenwerken met klassen (Implements)
Interfaces zijn ideaal om samen met klassen te gebruiken via het implements keyword, wat klassen dwingt om zich aan een specifieke structuur te houden.
interface Person {
name: string,
age: number,
isAdmin: boolean
}
interface Employee extends Person {
employeeId: number;
jobTitle: string;
}
// Object dat de interface implementeert
class Developer implements Employee {
name: string;
age: number;
isAdmin: boolean;
employeeIdL number;
jobTitle: string;
constructor(name: string, age: number, isAdmin, employeeId: number, jobTitle: string) {
this.name = name;
this.age = age;
this.employeeId = employeeId;
this.jobTitle = jobTitle;
}
}
Wanneer een type
Samengestelde types (Unions, Intersections)
Types kunnen union en intersection types definiëren, wat betekent dat je een variabele meerdere mogelijke types kunt geven of types kunt combineren. Dit is iets dat interfaces niet kunnen doen.
type PersonID = string | number; // Juist
interface EmployeeID = PersonID | undefined; // FOUT! Dit kan niet.
Type-aliasing
Types zijn flexibeler en kunnen worden gebruikt om aliasen te creëren voor elke mogelijke structuur in TypeScript, inclusief objecten, primitieve waarden (zoals string of number), functies, en complexere types zoals unions en tuples.
// Voor primitieve waarden
type PersonID = string | number;
Complexere types:
Als je werkt met complexere types, zoals functie-handtekeningen, tuples, of samengestelde types, is een type meestal handiger.
// Complex type voor een functie
type MathOperation = (a: number, b: number) => number;
const add: MathOperation = (x, y) => x + y;
// Union type
type Status = "success" | "error" | "pending";
let currentStatus: Status = "success"; // OK
currentStatus = "error"; // OK
// Intersection type
type User = { name: string };
type Admin = User & { privileges: string[] };
const admin: Admin = { name: "Bob", privileges: ["manage-users"] };
Wanneer kan allebei
Voor het definiëren van eenvoudige objectstructuren kun je zowel types als interfaces gebruiken, zoals in het onderstaande voorbeeld:
// Met interface
interface Address {
street: string;
city: string;
}
// Met type
type Address = {
street: string;
city: string;
};
In zulke gevallen is het vooral een kwestie van voorkeur, hoewel de flexibiliteit van interfaces om ze uit te breiden of samen te voegen een voordeel kan zijn in grotere codebases.
Strict null checking
Een andere krachtige eigenschap van Typescript is dat het ons toelaat om strict null checking te doen.
In Javascript kan het soms zijn dat een variabele ongewild null of undefined wordt omdat er ergens een foutje gemaakt is. In TypeScript ga je expliciet moeten definiëren of een variabele null of undefined mag zijn.
let myVariable: string | undefined;
Generics
Generics worden gebruikt om bepaalde meer generieke functies herbruikbaar te houden. Je legt wel bepaalde type restricties op maar je laat het nog wat open voor interpretatie en specifieke implementatie.
Onderstaande functie identity bijvoorbeeld is als volgt gedefinieerd:
Je kan er een argument van een type aan meegeven en het zal een return value hebben van datzelfde type.
Wanneer je dan outputString definieert geef je mee dat die vrij te kiezen type T een string zal zijn. Dat betekent dat vanaf dan, je aan outputString altijd een string moet meegeven en dat het altijd een string moet teruggeven. Hetzelfde geldt voor outputNumber behalve dat je daar expliciet zegt dat het een T een number zal zijn.
Je herbruikt dus dezelfde functie voor verschilllende types.
function identity<T>(arg: T): T {
return arg;
}
let outputString = identity<string>("Hello");
let outputNumber = identity<number>(10);
console.log(outputString); // Logt "Hello"
console.log(outputNumber); // Logt 10
Hetzelfde principe kan je ook toepassen op interfaces en classes:
interface Box<T> {
contents: T;
}
// Een box met een string-inhoud
const stringBox: Box<string> = { contents: "Hello, world!" };
console.log(stringBox.contents); // Output: Hello, world!
// Een box met een getal
const numberBox: Box<number> = { contents: 42 };
console.log(numberBox.contents); // Output: 42
// Een box met een object
const objectBox: Box<{ name: string, age: number }> = { contents: { name: "John", age: 30 } };
console.log(objectBox.contents.name); // Output: John
class GenericBox<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}
// Een GenericBox met een string
const stringGenericBox = new GenericBox<string>("Hello, TypeScript!");
console.log(stringGenericBox.value); // Output: Hello, TypeScript!
// Een GenericBox met een getal
const numberGenericBox = new GenericBox<number>(123);
console.log(numberGenericBox.value); // Output: 123
// Een GenericBox met een object
const objectGenericBox = new GenericBox<{ name: string, age: number }>({ name: "Alice", age: 25 });
console.log(objectGenericBox.value.name); // Output: Alice
Type guarding
Type guarding is een techniek in TypeScript waarmee je tijdens de uitvoering van je code kunt controleren wat het werkelijke type van een variabele is. Dit is vooral handig bij het werken met union types (waarbij een variabele meerdere mogelijke types kan hebben), omdat je zo bepaalde acties kunt uitvoeren afhankelijk van het exacte type dat de variabele heeft op een bepaald moment.
Wanneer gebruiken?
Je gebruikt type guards wanneer een variabele meerdere mogelijke types kan hebben en je specifieke logica wilt uitvoeren afhankelijk van het daadwerkelijke type. Dit helpt bij het veilig werken met union types en voorkomt runtime fouten.
function printId(id: string | number) {
if (typeof id === "string") {
console.log(`ID is a string: ${id.toUpperCase()}`); // Specifieke logica voor string
} else {
console.log(`ID is a number: ${id.toFixed(2)}`); // Specifieke logica voor number
}
}
In dit voorbeeld gebruikt typeof een type guard om te bepalen of id een string of number is, en voert vervolgens de bijpassende logica uit.
Oefeningen
Je kan de oefeningen voor deze module vinden op github in de volgende repo https://github.com/anneleenscholts/syntra-frontend-2-avo-oefeningen
De oefeningen specifiek voor TypeScript staan onder TypeScript